<?php 
/*

    real entities - a queriable text db

    Copyright (C) 2009 Eugeny Yakimovitch (at) gmail dot com

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.    
    
*/

if (!defined ('DIR_SEP'))
  define ('DIR_SEP', DIRECTORY_SEPARATOR);

define('ENTITY_POSTFIX','.event');
define('ENTITY_HASH_KEY_STR', md5('hashkey'));

define('QUERY_DATATYPE_DATETIME',1);
define('QUERY_DATATYPE_STRING',2);
define('QUERY_DATATYPE_INTEGER',3);
define('QUERY_DEFAULTS_FILE','event_defaults');

function query_cmp_entities($a, $b)
{
	global $query_cmp_fieldname, $query_cmp_datatype;

	$a = $a[$query_cmp_fieldname];
	$b = $b[$query_cmp_fieldname];
	
	switch ($query_cmp_datatype) 
	{
		case QUERY_DATATYPE_DATETIME:
			$cmp = 'query_datetime_cmp';
			break;
		case QUERY_DATATYPE_STRING:
			$cmp = 'query_string_cmp';
			break;
		case QUERY_DATATYPE_INTEGER:
			$cmp = 'query_int_cmp';
			break;
		default:
		trigger_error('Unknown data type',E_USER_ERROR);
	}
	return $cmp($a,$b);
}

function query_datetime_cmp($a, $b)
{
	$a = strtotime($a);
	$b = strtotime($b);
	if ($a == $b )
        {
		return 0;
        } 
	else if ($a < $b )
        {
		return -1;
        }
	return 1;
}

function query_string_cmp($a, $b)
{
	$a = (string) $a;
	$b = (string) $b;
	if ($a == $b )
        {
		return 0;
        } 
	else if ($a < $b )
        {
		return -1;
        }
	return 1;
}

function query_int_cmp($a, $b)
{
	$a = (int) $a;
	$b = (int) $b;
	if ($a == $b )
        {
		return 0;
        } 
	else if ($a < $b )
        {
		return -1;
        }
	return 1;
}

class Query
{

  var $entities = array();

  var $ignore_symlinks = true;

  var $url = '';

  function Query ($url)
  {	
    $this->url = $this->clean_path($url);
    # assume local path
    $this->parse_dir ($this->url);
  }

  function datatype($str)
  {
	$str = strtolower($str);
	switch ($str) {
		case 'datetime': return QUERY_DATATYPE_DATETIME;
		case 'string':	 return QUERY_DATATYPE_STRING;
		case 'int':	 return QUERY_DATATYPE_INTEGER;
		default:
		trigger_error('Unknown data type',E_USER_ERROR);
	}
  }

  function parse_dir ($path)
  {
    if (is_dir ($path))
      {
	if ($dh = opendir ($path))
	  {
	    while (($file = readdir ($dh)) !== false)
	      {
		if ($file == '.' || $file == '..')
			continue;
		if ($this->ignore_symlinks && is_link($path . DIR_SEP . $file))
			continue;
		if (is_dir ($path . DIR_SEP . $file)) 
                {
                  $this->parse_dir($path . DIR_SEP . $file);
                } 
                else if ($this->match_entity($entity = $path . DIR_SEP . $file))
                {                  
                  $entity = $this->parse_entity($entity); 
		  $this->entities[$entity[ENTITY_HASH_KEY_STR]] = $entity;
                }
	      }
	    closedir ($dh);
	  }
      }
  }

  function clean_path($path) 
  {
	if (preg_match('/\\' . DIR_SEP . '$/',$path))
		$path = substr($path,0,strlen($path) - 1);
	return $path;
  }

  function overwrite_entity($base, $cover)
  {
	foreach ($cover as $key => $val)
	{
		$base[$key] = $val;
	}
	return $base;
  }

  function get_defaults($path) 
  {
	$path = dirname($path);
	$dirs = $this->clean_path(substr($path,strlen($this->url)+1));
	$dirs = split(DIR_SEP,$dirs);
	$path = $this->url;
        $default_entity = array();
	$i = 0; do {
		if (file_exists($file = $path . DIR_SEP . QUERY_DEFAULTS_FILE ))
		{			
			$default_entity = $this->overwrite_entity($default_entity,$this->parse_entity($file,true));
		}	
		$dir = $dirs[$i++];
		$path .=  DIR_SEP . $dir;		
	} while ($i <= count($dirs));
	return $default_entity;
  }

  function match_entity($path)
  {
	$path = basename($path);
	return (preg_match('/'.ENTITY_POSTFIX.'$/',$path));
  }

  function parse_entity($path, $without_inheritance = false)
  {
	$entity = array();	
	if (!$without_inheritance)
		$entity = $this->get_defaults($path);
	//var_dump($entity);
	$lines = file($path);
	if (!is_array($lines))
		return;

	$end_of_field = '';
	$eof_found = true;
	foreach ($lines as $line) {
		if ($end_of_field == '') 
		{
			$tmp = trim($line);
			if (empty($tmp)) 
				continue;

			preg_match('/^([^\s\<]*)(\<\<[\S]*)?:[\s]*(.*)$/',$line,$matches);
		
			$key = strtolower($matches[1]);
			if (empty($key)) {				
				#malformed entity, report
				trigger_error( "Warning, ignoring malformed entity: $path! Empty key in line: [$line]" ,E_USER_WARNING);
				return;
			}

			if (!empty($matches[2])) {
				$end_of_field = substr($matches[2],2);
				$eof_found = false;
				$value = $matches[3];
			} else {
				$value = $matches[3];
				$entity[$key] = $value;
			}
		}
		else
		{
			if (preg_match('/^'.$end_of_field.'/',$line)) {
				$eof_found = true;
				$entity[$key] = $value;
			} else {
				$value .= $line;
			}
		}
	}

	if (!$eof_found) {
		#malformed entity, report
		trigger_error( "Warning, ignoring malformed entity: $path! No end of field symbol found" ,E_USER_WARNING);
		return;
	}

        if (!count($entity))
		return;

	$hash = $this->get_entity_hash($path);
	if (array_key_exists($hash,$this->entities)){
		trigger_error( "Warning, ignoring dupplicated entity: $path!" ,E_USER_WARNING);
		return;
	}
	$entity[ENTITY_HASH_KEY_STR] = $hash;
	return $entity;
  }

  function get_entity_hash($path)
  {
        return md5(file_get_contents($path));
  }

  function match_varname ($str) {
	return preg_match('/^\$?/',$str);
  }

  function parse_varname ($str) {
	preg_match('/^\$([^:]*):([\S]*)?/',$str, $m);
	return array($m[1],$m[2]);
  }

  function sort($field, $datatype)
  {
	global $query_cmp_fieldname, $query_cmp_datatype;
	$query_cmp_fieldname = $field;
	$query_cmp_datatype = (is_string($datatype)) ? $this->datatype($datatype) :  $datatype;
	usort($this->entities, query_cmp_entities);
	$result = array();
	foreach ($this->entities as $entity) 
		$result[$entity[ENTITY_HASH_KEY_STR]] = $entity;
	$this->entities = $result;
  }

  function filter($field, $value, $datatype, $cmp) 
  {
	$result = array();
	foreach ($this->entities as $hash => $entity) {
	  foreach ($entity as $en_field => $en_value) {				
		if ($en_field == $field) {
   		  $query_cmp_field_name = $en_field;
 		  switch ($datatype) 
		  {
			case QUERY_DATATYPE_DATETIME:
				$cmp_func = 'query_datetime_cmp';
				break;
			case QUERY_DATATYPE_STRING:
				$cmp_func = 'query_string_cmp';
				break;
			case QUERY_DATATYPE_INTEGER:
				$cmp_func = 'query_int_cmp';
				break;
			default:
			trigger_error('Unknown data type',E_USER_ERROR);
		  }
		  if ($cmp_func($en_value, $value) == $cmp) {
			$result[$hash] = $entity;			
		  }
		  break;
		}
   	  }
	}	
	$this->entities = $result;
  }

  # TODO: condition is an AND OR statement
  function where($condition)
  {
    	preg_match('/([^<>=]*)([<>=])(.*)/',$condition,$matches);
	
	$left = trim($matches[1]);
	$cmp = $matches[2];
	$right = trim($matches[3]);
	
	if ($cmp == '=') $cmp = 0;
	if ($cmp == '<') $cmp = -1;
	if ($cmp == '>') $cmp = 1;

	if ($this->match_varname($left))
	{
		list($a,$datatype) = $this->parse_varname($left);		
		$b = $right;
	} else if ($this->match_varname($right)) { 
		list($a,$datatype) = $this->parse_varname($right);
		$b = $left;
	} else {
		trigger_error('no variable inside where condition!', E_USER_ERROR);
	}
	$datatype = (empty($datatype)) ? $this->datatype('string') : $this->datatype($datatype) ;
	$this->filter($a, $b, $datatype, $cmp);
  }

  # obtains an intersection of keys of all entities, then removes keys that doesn't contained on intersection
  function normalize()
  {
  }

}

?>
